react 如何实现 requestIdleCallback
什么是 requestIdleCallback
requestIdleCallback 是浏览器提供的 API,可以让开发者在浏览器空闲时执行一些任务。
如何使用 requestIdleCallback
javascript
function requestIdleCallbackExample() {
if (window.requestIdleCallback) {
window.requestIdleCallback(function(time) {
if (time.remaining > 0) {
// do something
}
});
} else {
setTimeout(function() {
// do something
}, 0);
}
}
React 为什么要自己实现 requestIdleCallback
- requestIdleCallback 兼容性问题
- 为了更好的性能,浏览器 requestIdleCallback 是 50ms 执行一次,也就是 20 帧每秒,如果 React 使用这种渲染机制,那么会导致页面卡顿。
基于以上原因,React 自己实现了一个 requestIdleCallback,我们来看 Web 端的实现思路。
实现思路
- 我们要知道每一帧的时间间隔,也就是屏幕一帧需要的时间 假设是 16.6ms
- 从执行当前线程开始,屏幕更新 完成时间大概是 16.6ms 后
- 屏幕更新可能不需要 16.6ms,我们需要在屏幕更新后获取当前时间,用于判断是否有剩余时间
js
// 自执行函数
(function () {
let frameEndTime;
let penddingCallback;
// 时间切片的时间间隔 (react 默认是 5ms,也对外提供了修改的函数,我们可以根据设备的刷新率做取舍)
let yieldInterval = 16.6 // ms
// 获取此帧绘制完成的时机,MessageChannel
const channel = new MessageChannel();
requestIdleCallback = function (callback) {
penddingCallback = callback;
// rafTime requestAnimationFrame 回调执行的时间,说明本帧开始绘制
requestAnimationFrame((rafTime) => {
// 假设 需要 16.6ms 绘制完这一帧,那么预计绘制完成的时间是
frameEndTime = rafTime + yieldInterval;
// 随便发消息,以便在动画帧结束能获取消息
channel.port1.postMessage("raf cb execute");
});
};
channel.port2.onmessage = () => {
// requestAnimation 执行结束时机
// 当前帧可用时间
const time = timeRemaining();
// 如果有剩余时间
if (time > 0) {
penddingCallback &&
penddingCallback({
timeRemaining,
// 肯定没有超时
didTimeout: false,
});
} else {
// 简单粗暴处理 转移到下一帧处理
requestIdleCallback(penddingCallback);
}
};
function timeRemaining() {
return frameEndTime - performance.now();
}
})();
// 测试代码
requestIdleCallback((time) => {
console.log('剩余时间',time.timeRemaining(),'ms');
});
上面的 16.6 ms,对于不同设备是不一样的,刷新率比价高的设备上,会小于 16.6,会造成当前帧实际没有空闲时间,但是代码判断有空闲时间,会占用主任务时间。
我们来计算一下设备的 fps,简单思路:
js
/**
* @param targetCount 采样次数
* @param recalc 是否重新计算
*/
function getFPS(targetCount = 60, recalc) {
// 我们挂载到 __FPS__
//
if (window.__FPS__&&!recalc) return Promise.resolve(window.__FPS__)
const startTime = performance.now();
let count = targetCount;
return new Promise((resolve) => {
(function fps() {
requestAnimationFrame(() => {
count--;
if (count === 0) {
resolve(targetCount / ((performance.now() - startTime) / 1000));
return;
}
fps();
});
})();
});
}
getFPS().then(fps => {
console.log(fps, '帧率')
window.__FPS__ = fps
});
如果我们想处理精确的帧率,应该是算出每一帧的时间 也就是 1000 / window.__FPS__
但是React 是默认 5ms 的时间切片,如果这个时间不够,就会放弃当前任务去做高优先级的任务。
总结
requestIdleCallback
是 React 脱离平台的实现方案,不仅解决了浏览器的兼容问题,也能更精准的做到时间分片的控制,